package middlewares

import (
	"crypto/md5"
	"errors"
	"fmt"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/spf13/cast"
	global2 "longmen/server/config/global"
	"longmen/server/pkg/common/api_models"
	"longmen/server/pkg/common/helper/const"
	"longmen/server/pkg/common/helper/http"
	"longmen/server/pkg/common/helper/util"
	"reflect"
	"strings"
)

/**
 * @Author willi
 * @Description //TODO
 * @Date 15:17 2022/9/21
 * @Param
 * @return
 **/

type SignatureAuthConfig struct {
	BasicAuthSkipper middleware.Skipper
}

func SignatureConfig(config SignatureAuthConfig) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			path := c.Path()
			ip := getClientIp(c)
			orDefault := global2.Config.GetIntOrDefault("sign.open", 0)
			if paramsStr, ok := verifySign[path]; ok && !ipWhiteList(ip) && orDefault > 0 {
				signAuth := &api_models.SignAuth{}
				//解析参数
				method := getMethodName(path)
				if err := resolverParameter(c, method, signAuth); err != nil {
					return makeJson(SIGN_ERROR_CODE, "解析参数出错", err.Error(), c)
				}
				//fixme 以后这句话要去掉
				if len(signAuth.SignContent) > 0 {
					//限流
					if err := limitFlow(c, signAuth.Method, ip); err != nil {
						return makeJson(SIGN_ERROR_CODE, "操作太频繁", err.Error(), c)
					}
					//验签
					if err := generateAndVerifySign(c, signAuth, paramsStr); err != nil {
						return makeJson(SIGN_ERROR_CODE, "验签失败", err.Error(), c)
					}
				}
			}
			return next(c)
		}
	}
}

/**
 * @Author willi
 * @Description //根据url 获取方法名称
 * @Date 13:14 2022/9/28
 * @Param
 * @return
 **/
func getMethodName(path string) string {
	index := strings.LastIndex(path, URL_PATH_SPLIT)
	return path[index+1:]

}

/**
 * @Author
 * @Description // 解析BODY 中参数的值 如果 是特殊的接口，把需要叠加参数叠加到方法后面变成新的方法标识
 * @Date 14:23 2022/9/21
 * @Param
 * @return
 **/
func resolverParameter(c echo.Context, method string, signAuth *api_models.SignAuth) error {

	if err := util.Json2Struct(c.Get(_const.HTTP_KEY_BODY).(string), signAuth); err != nil {
		global2.Log.Error("sign解析json出错: ", err)
		return err
	}
	//如果是特殊的方法取上特殊字段的值合并到方法后面
	if methodName, ok := specialMethod[method]; ok {
		val := reflect.ValueOf(signAuth).Elem().FieldByName(methodName).Interface()
		signAuth.Method = method + fmt.Sprint(val)
	} else {
		signAuth.Method = method
	}
	return nil
}

/**
 * @Author 如果再次进入黑名单,会重新计时.防止刷接口
 * @Description // 加接口名称
 * @Date 14:23 2022/9/21
 * @Param
 * @return
 **/
func limitFlow(c echo.Context, method, ip string) error {
	stringLua := `if redis.call('EXISTS',KEYS[1])==1 then
    redis.call('EXPIRE',KEYS[1],ARGV[3])
    return 2
else
   local val=redis.call('GET',KEYS[2])
   if val and tonumber(val)>tonumber(ARGV[1]) then
      redis.call("SET",KEYS[1],'')
      redis.call('EXPIRE',KEYS[1],ARGV[3])
      return 1
   else
      redis.call('INCR',KEYS[2])
      redis.call('EXPIRE',KEYS[2],ARGV[2])
      return 0
   end
 end `
	blockKey := generateCacheKey(SIGN_BLOCK, method, ip)
	limitKey := generateCacheKey(SIGN_LIMIT, method, ip)
	if i, err := global2.GetRds().Eval(stringLua, []string{blockKey, limitKey}, LIMIT_FREQUENCY, LIMIT_UNIT, BLOCK_TIME_UNIT).Int(); err != nil {
		global2.Log.Error("lua 脚本执行错误,redis繁忙: ", err)
		return err
	} else if i == 1 {
		msg := fmt.Sprintf("ip:%s,在%d秒之内操作%d次,操作太频繁,参数:%s", ip, LIMIT_UNIT, LIMIT_FREQUENCY, c.Get(_const.HTTP_KEY_BODY).(string))
		global2.Log.Error(msg, err)
		return errors.New(msg)
	} else if i == 2 {
		msg := fmt.Sprintf("ip:%s,操作太频繁,%d秒之后请重试,参数:%s", ip, BLOCK_TIME_UNIT, c.Get(_const.HTTP_KEY_BODY).(string))
		global2.Log.Error(msg, err)
		return errors.New(msg)
	}
	return nil
}

/**
 * @Author admin
 * @Description // 获取IP地址
 * @Date 18:45 2022/9/21
 * @Param
 * @return
 **/
func getClientIp(c echo.Context) string {
	ip := c.RealIP()
	if strings.EqualFold(ip, "::1") {
		externalIP, _ := http.ExternalIP()
		ip = cast.ToString(externalIP)
	}
	return ip
}

/**
 * @Author
 * @Description //
 * @Date 13:51 2022/9/21
 * @Param
 * @return
 **/
func generateCacheKey(prefix, method, ip string) string {
	return fmt.Sprintf("%s:%s:%s", prefix, method, ip)
}

/*
*

	1。算法如下：根据版本信息找到对应版本的干扰码
	2。客户端取当前unix 时间戳 加 用户id(如未有登陆为0)  除以干扰码长度 取余
	3。在干扰码中找到取余位置下标的字符
	4。验签字符串生成 取方法名，作户id,当前unix时间戳，版本号 组成如例(method=getPaymentList&uid=1011231&signDate=1663730880&version=V1)  如大接口需特别处理 method=  getPaymentList+某一个类别参数
	5.验签字符串生成MD5码
	6.客户端取当前unix 时间戳 加 用户id(如未有登陆为0)  除以32 取余
	7.根据取余的值在验签md5码中找到对应的字符
	8.在md5码中使用干扰码中找出的字符替换在MD5中找出的字符
	9.完成生成签符字符串

* @Author willi
* @Description //TODO
* @Date 15:17 2022/9/21
* @Param
* @return
*
*/
func generateAndVerifySign(c echo.Context, signAuth *api_models.SignAuth, paramsStr string) error {
	if len(signAuth.SignVersion) == 0 {
		signAuth.SignVersion = "V1"
	}
	if keys, ok1 := versionMap[signAuth.SignVersion]; ok1 {
		signStr, err := resSingStr(c, signAuth, paramsStr)
		if err != nil {
			return err
		}
		//fixme  此句上线应取消
		//global.Log.Debug(signStr)
		sumMd5 := md5.Sum([]byte(signStr))
		md5Str := fmt.Sprintf("%x", sumMd5)
		//转换成大写
		md5Str = strings.ToTitle(md5Str)
		signAuth.DisturbCode = getDisturbCodeNum(md5Str)
		//从扰乱码中找到字符
		s := (signAuth.SignDate + int64(signAuth.DisturbCode)) % int64(len(keys))
		//找到MD5中需要替换的字符
		r := (signAuth.SignDate + int64(signAuth.DisturbCode)) % 32
		tarStr := md5Str[r : r+1]
		scrStr := keys[s : s+1]
		md5Str = strings.ReplaceAll(md5Str, tarStr, scrStr)
		md5Str = strings.ToTitle(md5Str)
		//fixme  此句上线应取消
		//global.Log.Debug(fmt.Sprintf("扰乱码中字符:%s md5中的字符:%s  原始MD5:%s", scrStr, tarStr, md5Str))
		if strings.EqualFold(md5Str, signAuth.SignContent) {
			return nil
		}
		msg := fmt.Sprintf("sginStr:=%s md5Str:=%s", signStr, md5Str)
		global2.Log.Error(msg, errors.New("验签错误"))
		return errors.New("验签错误,客户端加密原始字符串为:" + signAuth.OriginalStr + " 服务器端加密原始字符串为:" + signStr)
	}
	global2.Log.Error(signAuth.SignVersion, errors.New("没有此版本扰乱码"))
	return errors.New("没有此版本扰乱码")
}

/**
 * @Author admin
 * @Description //TODO
 * @Date 15:17 2022/9/21
 * @Param
 * @return
 **/

func resSingStr(c echo.Context, signAuth *api_models.SignAuth, paramsStr string) (string, error) {
	var strBuilder strings.Builder
	strBuilder.WriteString("method=")
	strBuilder.WriteString(signAuth.Method)
	/*	strBuilder.WriteString("&uid=")
		strBuilder.WriteString(cast.ToString(signAuth.Uid))*/
	strBuilder.WriteString("&signDate=")
	strBuilder.WriteString(cast.ToString(signAuth.SignDate))
	strBuilder.WriteString("&sign_version=")
	strBuilder.WriteString(signAuth.SignVersion)

	if extraStr, err := mappingParamAndValue(c, paramsStr); err == nil {
		strBuilder.WriteString(extraStr)
		return strBuilder.String(), nil
	} else {
		return "", err
	}
}

/**
 * @Author willi
 * @Description //TODO
 * @Date 12:56 2022/9/27
 * @Param
 * @return
 **/
func mappingParamAndValue(c echo.Context, paramsStr string) (string, error) {
	params := make(map[string]interface{})
	if err := util.Json2Struct(c.Get(_const.HTTP_KEY_BODY).(string), &params); err == nil {
		var strBuilder strings.Builder
		splitArr := strings.Split(paramsStr, PARAMS_SPLIT)
		for _, paramsName := range splitArr {
			if len(paramsName) > 0 {
				if val, ok := params[paramsName]; ok {
					strBuilder.WriteString("&" + paramsName + "=")
					strBuilder.WriteString(cast.ToString(val))
				}
			}
		}
		return strBuilder.String(), nil
	} else {
		return "", err
	}
}

/**
 * @Author willi
 * @Description //TODO
 * @Date 15:17 2022/9/21
 * @Param
 * @return
 **/
func makeJson(code int, msg, ErrMsg string, c echo.Context) error {
	return c.JSON(code, api_models.Response{
		Status: 1,
		Msg:    msg,
		ErrMsg: ErrMsg,
	})
}

/**
 * @Author willi
 * @Description //
 * @Date 9:43 2022/9/22
 * @Param
 * @return
 **/
func ipWhiteList(ip string) bool {
	split := strings.Split(ip, ".")
	length := len(split)
	val := split[length-1]
	bucket := 0
	if len(val) > 0 {
		bucket = cast.ToInt(val) % 10
	}
	key := fmt.Sprintf("%s:%d", SIGN_WHITE, bucket)
	b := global2.GetRds().HExists(key, ip).Val()
	return b
}

func getUid(c echo.Context) int {
	var uid int
	uid = cast.ToInt(util.GetCookie(c, "uid"))
	authorization := strings.Split(c.Request().Header.Get("authorization"), ";")
	if len(authorization) >= 2 { //截取请求头中 token 和 uid
		uidSlice := strings.Split(authorization[1], "=")
		if len(uidSlice) >= 2 {
			uid = cast.ToInt(uidSlice[1])
		}
	}
	return uid
}

/**
 * @Author willi
 * @Description //TODO
 * @Date 13:57 2022/9/26
 * @Param
 * @return
 **/
func getDisturbCodeNum(md5Str string) int {
	toNum := 0
	bytes := []byte(md5Str)
	word := int(bytes[31])
	num := word % 2
	for i := 0; i < 5; i++ {
		toNum += int(bytes[i*2+num])
	}
	return toNum
}
